package jstart.exe;

import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URLConnection;
import java.util.Enumeration;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.swing.*;

/**
 * @author zwz
 * Created on 2021-02-08
 */
public class UpdateLauncher extends DefaultLauncher {

    private final AtomicBoolean busy;
    private UpdateDialog dialog;
    private String remoteUpdateFile;

    UpdateLauncher(LaunchContext context) {
        super(context);
        busy = new AtomicBoolean(false);
    }

    @Override
    public void launch() {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (Exception ignored) {
            // ignored
        }
        dialog = new UpdateDialog();
        dialog.updateProgress("更新尚未开始", 0);

        dialog.btnOK.setText("检查更新");
        dialog.btnOK.addActionListener(this::actionPerformed);

        dialog.btnCancel.addActionListener(this::actionPerformed);

        dialog.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                dialog.btnCancel.doClick();
            }
        });
        dialog.setVisible(true);
    }

    private void actionPerformed(ActionEvent evt) {
        if (dialog.btnCancel.equals(evt.getSource())) {
            if (dialog.btnOK.getText()
                            .equals("完成")) {
                dialog.dispose();
                System.exit(0);
            } else {
                if (JOptionPane.showConfirmDialog(dialog, "在更新过程中退出将导致更新不完整，是否退出？", "提示",
                                                  JOptionPane.YES_NO_OPTION)
                        == JOptionPane.YES_OPTION) {
                    busy.set(false);
                    dialog.dispose();
                    System.exit(0);
                }
            }
            return;
        }

        if (busy.get()) return;
        try {
            switch (dialog.btnOK.getText()) {
                case "检查更新":
                    checkForUpdate();
                    break;
                case "更新":
                    doUpdate();
                    break;
                case "完成":
                    busy.set(false);
                    dialog.dispose();
                    System.exit(0);
                    break;
            }
        } catch (Exception e) {
            Kits.showMessageBox(dialog, true, e.getMessage());
        }
    }

    private void checkForUpdate() throws IOException {
        dialog.btnOK.setEnabled(false);
        String versionCheckUrl = getProperty("update.versionCheckUrl", "");
        if (Kits.isNotVisibleString(versionCheckUrl)) {
            throw new RuntimeException("版本检查地址(update.versionCheckUrl)无效！");
        }

        // 获取本地版本信息
        Properties localProps = new Properties();
        Kits.loadProps(localProps, new File(context.getBootDir(), "version.txt"));
        final int localVersion = Integer.parseInt(localProps.getProperty("versionCode", "10000"));

        URLConnection connection = openUrlConnection(versionCheckUrl);
        dialog.updateProgress("正在检查新版本...", 0);
        Properties remoteProps = new Properties();
        Kits.loadProps(remoteProps, connection.getInputStream());

        final int remoteVersion = Integer.parseInt(remoteProps.getProperty("versionCode"));
        if (localVersion >= remoteVersion) {
            dialog.updateProgress("已是最新版本无需更新！", 100);
            throw new RuntimeException("已是最新版本无需更新！");
        }

        remoteUpdateFile = remoteProps.getProperty("file");
        if (Kits.isNotVisibleString(remoteUpdateFile)) {
            throw new RuntimeException("从远程获取的文件名无效！");
        }
        dialog.updateProgress("发现新版本：" + remoteProps.getProperty("version"), 0);
        dialog.btnOK.setText("更新");
        dialog.btnOK.setEnabled(true);
    }

    private void doUpdate() {
        if (busy.get()) return;
        busy.set(true);
        dialog.btnOK.setEnabled(false);
        dialog.btnCancel.setEnabled(false);
        new Thread(() -> {
            try {
                File file = downloadUpdate();
                applyUpdate(file);
            } catch (IOException e) {
                dialog.updateProgress("更新失败：\n" + e.getMessage(), 100);
                Kits.showMessageBox(dialog, true, "更新失败：\n" + e.getMessage());
            } finally {
                busy.set(false);
                dialog.btnOK.setText("完成");
                dialog.btnOK.setEnabled(true);
            }
        }).start();
    }

    private URLConnection openUrlConnection(String url) throws IOException {
        String authorization = getProperty("update.authorization", "");
        URLConnection connection = URI.create(url)
                                      .toURL()
                                      .openConnection();
        if (Kits.isVisibleString(authorization)) {
            connection.setRequestProperty("Authorization", authorization);
        }
        return connection;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private File downloadUpdate() throws IOException {
        String downloadUrl = getProperty("update.fileDownloadUrl", "");
        if (Kits.isNotVisibleString(downloadUrl)) {
            throw new RuntimeException("更新下载地址无效！");
        }

        File downloadTo =
                new File(context.getBootDir(), getProperty("update.downloadTo", "update"));
        if (!downloadTo.exists()) {
            downloadTo.mkdirs();
        }
        File downloadFile = new File(downloadTo, remoteUpdateFile);
        FileOutputStream out = new FileOutputStream(downloadFile);
        URLConnection connection =
                openUrlConnection(String.format(downloadUrl, remoteUpdateFile));
        long contentLength = connection.getContentLengthLong();
        long count = 0;
        byte[] bytes = new byte[4096]; // 4k
        int len;
        InputStream ins = connection.getInputStream();
        while ((len = ins.read(bytes)) > 0) {
            out.write(bytes, 0, len);
            count += len;
            if (contentLength > 0) {
                dialog.updateProgress("正在下载文件：" + remoteUpdateFile,
                                      (int) (100.0 * count / contentLength));
            } else {
                dialog.updateProgress("正在下载文件：" + remoteUpdateFile + "\n" + count + " bytes.", 0);
            }
        }
        out.close();
        ins.close();

        if (count == 0) {
            throw new RuntimeException("文件下载失败！");
        } else {
            dialog.updateProgress("文件下载完成。", 100);
        }
        return downloadFile;
    }

    @SuppressWarnings("ResultOfMethodCallIgnored")
    private void applyUpdate(File updateFile) throws IOException {
        File unzipToDir = context.getBootDir();
        String unzipTo = getProperty("update.unzipTo", "app");
        if (Kits.isVisibleString(unzipTo)) {
            unzipToDir = new File(context.getBootDir(), unzipTo);
        }

        if (!context.getBootDir()
                    .equals(unzipToDir)) {
            // empty unzipToDir
            if (unzipToDir.exists()) {
                Kits.emptyDir(unzipToDir);
            } else {
                unzipToDir.mkdirs();
            }
        }

        ZipFile zipFile = new ZipFile(updateFile);
        Enumeration<? extends ZipEntry> entries = zipFile.entries();
        InputStream ins;
        OutputStream out;
        byte[] bytes = new byte[4096]; // 4k
        int len;
        while (entries.hasMoreElements()) {
            ZipEntry zipEntry = entries.nextElement();
            File target = new File(unzipToDir, zipEntry.getName());
            if (zipEntry.isDirectory()) {
                target.mkdirs();
                dialog.updateProgress("创建目录：" + target.getName(), 100);
            } else {
                ins = zipFile.getInputStream(zipEntry);
                out = new FileOutputStream(target);
                dialog.updateProgress("解压文件：" + zipEntry.getName(), 0);
                while ((len = ins.read(bytes)) > 0) {
                    out.write(bytes, 0, len);
                }
                out.close();
                ins.close();
                dialog.updateProgress("解压文件：" + zipEntry.getName() + " 完成。", 100);
            }
            target.setLastModified(zipEntry.getLastModifiedTime()
                                           .toMillis());
        }
        dialog.updateProgress("更新完成。", 100);
    }
}
